import java.io.IOException;
import java.util.ArrayList;

import libsvm.*;


public class main {
	public static void main(String[] args) {
		boolean train = true;	// Sets whether or not to train the classifier
		boolean verbose = false;	// If true the classifier will print a lot of data while training
		int max = 30000;		// Maximum number of images to use
		int cutoff = 10000;		// Number of images to train with. The rest will be used to test.
		String labelFile = "train-labels.idx1-ubyte"; // The file that holds the labels for the training data
		String imageFile = "train-images.idx3-ubyte"; // The file that holds the images for training
		String modelFile = "model.svm"; // The filename for the classifier model 
		boolean showMisses = false;	// Show the one missed pattern for each digit
		svm_model model = new svm_model();
		if (train) {
			ReadMNIST reader = ReadMNIST.getInstance();
			reader.setFiles(imageFile, labelFile);
			int[] labels;
			int hits = 0;
			int dots = 0;
	
			long start = System.currentTimeMillis();
			try {
				int[] trainingSet = new int[10];
				System.out.println("Reading data: ");
				labels = reader.readLabels();
				svm_problem problem = new svm_problem();
				problem.l = cutoff;
				problem.y = new double[cutoff];
				ArrayList<svm_node[] > nodes = new ArrayList<svm_node[]>();
				
				for(int i=0; i!=cutoff; i++) {
					if ((float)i/cutoff >= (float)dots/10) {
						System.out.print("-");
						dots++;
					}
					int[][] pattern = reader.getNextImage();
					int digit = labels[i];
					trainingSet[digit]++;
					problem.y[i] = digit;
	
					FeatureVector vector = processPattern(pattern);
					nodes.add(vector.getNodes());
				}
				svm_node[][] nodeArr = new svm_node[cutoff][];
				System.out.println("\nTraining images per digit: ");
				for(int i=0;i!=10;i++) {
					System.out.println(i + ": " + trainingSet[i]);
				}
				System.out.println();
				problem.x = nodes.toArray(nodeArr); 
				svm_parameter params = new svm_parameter();
				
				// Parameters for the SVM classifier
				params.svm_type = svm_parameter.C_SVC;
				params.kernel_type = svm_parameter.RBF;
				params.cache_size = 256;
				params.eps = 0.001;
				params.gamma = Math.pow(2, -17);	// Determine earlier with a grid search
				params.C = 2048;					// Determine earlier with a grid search
				params.probability = 1;				// Record probability
				if (!verbose)
					svm.svm_set_print_string_function(new svm_print_interface() {
						
						@Override
						public void print(String s) {	
							if (s.equals("*"))
								System.out.print(s);
						}
					});
				
				model = new svm_model();
				model = svm.svm_train(problem, params);
				svm.svm_save_model(modelFile, model);
				
				int[][] perDigit = new int[10][2];
				int[][] confMat = new int[10][10];
				boolean[] misses = new boolean[10];
				boolean missed = false;
				System.out.println("\nClassifying data: ");
				for(int i=cutoff; i!=max; i++) {
					int[][] pattern = reader.getNextImage();
					int[][] oldPattern = pattern;
					int digit = labels[i];
					FeatureVector vector = processPattern(pattern);
					double result = svm.svm_predict(model, vector.getNodes());
					perDigit[digit][0]++;
					confMat[digit][(int) result]++;
					if (result == digit){
						perDigit[digit][1]++;
						hits++;
					}
					else {
						if(!misses[digit] && showMisses) {
							misses[digit] = true;
							for(int x=0; x!=28; x++) {
								for(int y=0; y!=28; y++){
									System.out.print(oldPattern[x][y] + " ");
									
								}
								System.out.println();
							}
							System.out.println("Guess: " + (int)result + " Actual: " + digit);
							if (!missed) {
								double[] prob = new double[10];
					    		int[] classes = model.label;
					    		int[] order = new int[10];
					    		for(int p=0; p!=10; p++) {
					    			order[classes[p]] = p;
					    		}
					    		svm.svm_predict_probability(model, vector.getNodes(), prob);
					    		for(int p=0; p!=10; p++) {
					    			System.out.println(p + ": " + ((double)Math.round(prob[order[p]]*100000)/1000) + "%");
					    		}
					    		missed = true;
							}
							System.out.println();
						}
					}
				}
				System.out.println("Accuracy: ");
				System.out.println("Overall: " + hits + "/" + (max-cutoff) + " - " + ((float)hits/(max-cutoff)*100) + "%");
				for(int i=0; i!=10; i++){
					System.out.println(i + ": " +  perDigit[i][1] + " / " + perDigit[i][0] + " - " + ((float)perDigit[i][1] / perDigit[i][0]));
				}
				System.out.println("\nConfusion Matrix:");
				for(int i=0; i!=10; i++) {
					System.out.print(i + ": ");
					for(int j=0; j!=10;j++) {
						int val = confMat[i][j];
						System.out.print((val < 10 ? "   " : val < 100 ? "  " : val < 1000 ? " " : "") + val + " ");
					}
					System.out.println();
				}
	
				long end = System.currentTimeMillis();
	//			System.out.println(start + " - " + end + " = " + (end-start));
				long h = ((end-start)/1000)/3600;
				long m = (((end-start)/1000)-h*60)/60;
				long s = ((end-start)/1000-h*60*60-m*60);
				System.out.println("Training completed in: " + h + ":" + (m < 10 ? "0" : "") 
						+ m + ":" + (s < 10 ? "0" : "") + s);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		else {
			try {
				model = svm.svm_load_model(modelFile);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		DigitReader.createGUI(model);
	}
	
	public static void printPattern(int[][] pattern) {
		for(int y=0; y!=pattern[0].length; y++){
			for(int x=0; x!=pattern.length; x++)
			{
				System.out.print(pattern[x][y] == 0 ? " " : pattern[x][y] >= 127 || pattern[x][y] == 1 ? "x" : ".");
			}
			System.out.println();
		}
	}
	
	// Simply runs through all the steps 
	public static FeatureVector processPattern(int[][] pattern) {

		pattern = Processing.slantCorrection(pattern);
		pattern = Processing.gaussianBlur(pattern, 0.5);
		pattern = GeneralFunctions.binarize(pattern);
		pattern = Processing.fill(pattern, 0, true, false);
		pattern = Processing.thin(pattern, true, false);
		pattern = Processing.normalization(pattern, 32, 32, 3);
		FeatureVector vector = new FeatureVector(8, 8, 32, 32);
		vector.setDensityPerRegion(PatternProcessor.densityPerRegion(8, 8, pattern));
		vector.setProfiles(FeatureExtraction.profiles(pattern, "", false, true));
		// Unused features
//		vector.setTopHalfPercent(PatternProcessor.percentTopHalf(pattern));
//		vector.setLeftHalfPercent(PatternProcessor.percentLeftHalf(pattern));
//		vector.setVerticalReflect(PatternProcessor.reflectedVertical(pattern));
//		vector.setHorizontalReflect(PatternProcessor.reflectedHorizontal(pattern));
		vector.setAspectRatio(FeatureExtraction.aspectRatio(pattern, false));
		vector.setAverageDistanceFromCenter(FeatureExtraction.averageDistanceFromCenter(pattern, false));
		
		pattern = Processing.skeletonize(pattern, 1, false);
		pattern = Processing.removeSoloPoints(pattern);
		vector.setEndPoints(FeatureExtraction.findEndPoints(pattern, "", true, false));
		return vector;
	}

}
